package com.snda.sc.utils;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.snda.sc.exception.ConfigureException;

/**
 * 文件修改监听，仅监听目录下第一层文件
 * 
 * @author lixi.atlantis
 * 
 */
public class FileWatcher {

	public static interface FileWatcherAction {

		void execute(Path modified);
	}

	private Map<Path, String> md5Contents = new HashMap<>();
	private WatchService ws = FileSystems.getDefault().newWatchService();
	private Path dirPath;
	private FileWatcherAction action;
	private boolean isStarted = false;

	public FileWatcher(String watchDir, FileWatcherAction action) throws IOException {
		this(Paths.get(watchDir), action);
	}

	public FileWatcher(Path dirPath, FileWatcherAction action) throws IOException {
		if (!Files.exists(dirPath) || !Files.isDirectory(dirPath)) throw new ConfigureException(String.format(
				"'%s' is not a directory or not existed.", dirPath));
		this.dirPath = dirPath;
		this.action = action;
	}

	public void watch() throws IOException {
		if (isStarted) return;
		isStarted = true;
		filterFiles(dirPath);
		dirPath.register(ws, ENTRY_MODIFY);
		new Thread(new Runnable() {
			@Override
			public void run() {
				loopFileEvent();
			}
		}).start();
	}

	public void destroy() throws IOException {
		isStarted = false;
		ws.close();
		md5Contents.clear();
	}

	private void loopFileEvent() {
		while (isStarted) {
			WatchKey key = null;
			try {
				key = ws.take();
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				continue;
			}
			List<WatchEvent<?>> events = key.pollEvents();
			Path parent = (Path) key.watchable();
			for (WatchEvent<?> event : events) {
				Path path = parent.resolve((Path) event.context());
				String fileMd5 = md5File(path);
				String oldFileMd5 = md5Contents.get(path);
				if (fileMd5 == null || oldFileMd5 == null || fileMd5.equals(oldFileMd5)) continue;
				md5Contents.put(path, fileMd5);
				Logs.logSysInfo("'{}' has been modified. Prepare to execute {}", path, action);
				action.execute(path);
			}
			if (!key.reset()) break;
		}
	}

	private void filterFiles(Path dirPath) throws IOException {
		Files.walkFileTree(dirPath, EnumSet.noneOf(FileVisitOption.class), 1, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
				if (Files.isDirectory(file)) return FileVisitResult.CONTINUE;
				String fileMd5 = md5File(file);
				md5Contents.put(file, fileMd5);
				return FileVisitResult.CONTINUE;
			}
		});
	}

	private String md5File(Path path) {
		try {
			return CommonUtils.md5(Files.readAllBytes(path));
		} catch (IOException e) {
			Logs.logException("Unable to read file {}, caused by: {}", path, e);
			return null;
		}
	}
}
